#ifndef SEPARABLESSS_H
#define SEPARABLESSS_H

#include <vector>
#include <glm/glm.hpp>

#include "RenderTarget.h"
#include "Shader.h"
#include "RenderEngine.h"

#define SSS_N_SAMPLES 17

class SeparableSSS
{
public:
	SeparableSSS() {};

	void init(
		//int width, int height,
		float fovy, float sssWidth, int nSamples = 17, bool stencilInitialized = true,
		bool followShape = true, bool separateStrengthSource = false)
	{
		//_width = width;
		//_height = height;
		this->sssWidth = sssWidth;
		this->nSamples = nSamples;
		this->stencilInitialized = stencilInitialized;
		this->strength = glm::vec3(0.48f, 0.41f, 0.28f);
		this->falloff = glm::vec3(1.0f, 0.37f, 0.3f);

		_tmpRT.init(RenderContex::window_width, RenderContex::window_height, GL_SRGB8_ALPHA8);
		//calculate_kernel();
	}

	void resize(int width, int height)
	{
		_tmpRT.resize(width, height);
	}

	static void static_init()
	{
		shader.init(IOS_PATH("shader", "Quad", "vert"), IOS_PATH("shader", "SSSS", "frag"));
	}

	void render(RenderTexture & colorTex, RenderTexture & depthTex, RenderTexture & strengthTex, DepthStencil & depthStencilRT) const
	{
		shader.use();
		shader.bind_float("sssWidth", sssWidth);
		shader.bind_texture("depthTex", depthTex, 0);
		shader.bind_texture("strengthTex", strengthTex, 1);
		
		// TODO: bind per frame???
		//shader.bind_vec4_array("kernel", kernel);

		glEnable(GL_STENCIL_TEST);
		// run the horizontal pass:
		glStencilFunc(GL_EQUAL, 1, 0xFF);
		glStencilMask(0x00);
		glDisable(GL_DEPTH_TEST);

		RenderEngine::set_render_target(&_tmpRT, &depthStencilRT);
		glClear(GL_COLOR_BUFFER_BIT);
		shader.bind_texture("colorTex", colorTex, 2);
		shader.bind_vec2("dir", vec2(1.0f, 0.0f));
		shader.bind_bool("initStencil", false);
		ModelManager::screen_aligned_quad.render();
		//RenderEngine::set_render_target(nullptr, nullptr);

		// and finish with the vertical one:
		//shader.use();
		//shader.bind_float("sssWidth", sssWidth);
		//shader.bind_texture("depthTex", depthTex, 0);
		//shader.bind_texture("strengthTex", strengthTex, 1);
		// TODO: bind per frame???
		//shader.bind_vec4_array("kernel", kernel);
		RenderEngine::set_render_target(&colorTex, &depthStencilRT);
		//glClear(GL_COLOR_BUFFER_BIT);
		shader.bind_texture("colorTex", _tmpRT, 2);
		shader.bind_vec2("dir", vec2(0.0f, 1.0f));
		//shader.bind_int("init_stencil", 0);
		ModelManager::screen_aligned_quad.render();
		//RenderEngine::set_render_target(nullptr, nullptr);

		glDisable(GL_STENCIL_TEST);
		glEnable(GL_DEPTH_TEST);
	}

	/**
	* This parameter specifies the global level of subsurface scattering,
	* or in other words, the width of the filter.
	*/
	void setWidth(float width) { this->sssWidth = width; }
	float getWidth() const { return sssWidth; }

	/**
	* @STRENGTH
	*
	* This parameter specifies the how much of the diffuse light gets into
	* the skin, and thus gets modified by the SSS mechanism.
	*
	* It can be seen as a per-channel mix factor between the original
	* image, and the SSS-filtered image.
	*/
	void setStrength(vec3 strength) 
	{
		if (glm::distance(strength, this->strength) > 0.1f)
		{
			this->strength = strength; 
			calculate_kernel(); 
		}
		
	}
	vec3 getStrength() const { return strength; }

	/**
	* This parameter defines the per-channel falloff of the gradients
	* produced by the subsurface scattering events.
	*
	* It can be used to fine tune the color of the gradients.
	*/
	void setFalloff(vec3 falloff) 
	{
		if (glm::distance(falloff, this->falloff) > 0.1f)
		{
			this->falloff = falloff;
			calculate_kernel(); 
		}
		
	}
	vec3 getFalloff() const { return falloff; }

	std::string get_kernel_code() const;

private:
	glm::vec3 gaussian(float variance, float r)
	{
		glm::vec3 g;
		for (int i = 0; i < 3; i++)
		{
			float rr = r / (0.001f + falloff[i]);
			g[i] = exp((-(rr * rr)) / (2.0f * variance)) / (2.0f * 3.14f * variance);
		}
		return g;
	}

	glm::vec3 profile(float r)
	{
		return  // 0.233f * gaussian(0.0064f, r) + /* We consider this one to be directly bounced light, accounted by the strength parameter (see @STRENGTH) */
			0.100f * gaussian(0.0484f, r) +
			0.118f * gaussian(0.187f, r) +
			0.113f * gaussian(0.567f, r) +
			0.358f * gaussian(1.99f, r) +
			0.078f * gaussian(7.41f, r);
	}

	void calculate_kernel()
	{
		Debug::LogInfo("SeparableSSS::calculate_kernel");
		const float RANGE = nSamples > 20 ? 3.0f : 2.0f;
		const float EXPONENT = 2.0f;

		kernel.resize(nSamples);

		// Calculate the offsets:
		float step = 2.0f * RANGE / (nSamples - 1);
		for (int i = 0; i < nSamples; i++) {
			float o = -RANGE + float(i) * step;
			float sign = o < 0.0f ? -1.0f : 1.0f;
			kernel[i].w = RANGE * sign * std::abs(pow(o, EXPONENT)) / pow(RANGE, EXPONENT);
		}

		// Calculate the weights:
		for (int i = 0; i < nSamples; i++) {
			float w0 = i > 0 ? std::abs(kernel[i].w - kernel[i - 1].w) : 0.0f;
			float w1 = i < nSamples - 1 ? std::abs(kernel[i].w - kernel[i + 1].w) : 0.0f;
			float area = (w0 + w1) / 2.0f;
			glm::vec3 t = area * profile(kernel[i].w);
			kernel[i].x = t.x;
			kernel[i].y = t.y;
			kernel[i].z = t.z;
		}

		// We want the offset 0.0 to come first:
		glm::vec4 t = kernel[nSamples / 2];
		for (int i = nSamples / 2; i > 0; i--)
			kernel[i] = kernel[i - 1];
		kernel[0] = t;

		// Calculate the sum of the weights, we will need to normalize them below:
		glm::vec3 sum(0.0f, 0.0f, 0.0f);
		for (int i = 0; i < nSamples; i++)
			sum += glm::vec3(kernel[i]);

		// Normalize the weights:
		for (int i = 0; i < nSamples; i++) {
			kernel[i].x /= sum.x;
			kernel[i].y /= sum.y;
			kernel[i].z /= sum.z;
		}

		// Tweak them using the desired strength. The first one is:
		//     lerp(1.0, kernel[0].rgb, strength)
		kernel[0].x = (1.0f - strength.x) * 1.0f + strength.x * kernel[0].x;
		kernel[0].y = (1.0f - strength.y) * 1.0f + strength.y * kernel[0].y;
		kernel[0].z = (1.0f - strength.z) * 1.0f + strength.z * kernel[0].z;

		// The others:
		//     lerp(0.0, kernel[0].rgb, strength)
		for (int i = 1; i < nSamples; i++) {
			kernel[i].x *= strength.x;
			kernel[i].y *= strength.y;
			kernel[i].z *= strength.z;
		}

		// Finally, set 'em!
		//V(kernelVariable->SetFloatVectorArray((float *)&kernel.front(), 0, nSamples));
	}

	float sssWidth;
	int nSamples;
	bool stencilInitialized;
	glm::vec3 strength;
	glm::vec3 falloff;

	RenderTexture _tmpRT;

	std::vector<glm::vec4> kernel;

	static Shader shader;
};



#endif